<template>
	<view>
		<template v-if="showLoading">
			<view class="ly-loader ly-flex-center"><view class="ly-loader-inner">加载中...</view></view>
		</template>
		<template v-else>
			<view v-if="isEmpty || !visible" class="ly-empty">{{ emptyText }}</view>
			<view :key="updateKey" class="ly-tree" :class="{ 'is-empty': isEmpty || !visible }" role="tree" name="LyTreeExpand">
				<ly-tree-node
					v-for="nodeId in childNodesId"
					:nodeId="nodeId"
					:render-after-expand="renderAfterExpand"
					:show-checkbox="showCheckbox"
					:show-radio="showRadio"
					:check-only-leaf="checkOnlyLeaf"
					:key="getNodeKey(nodeId)"
					:indent="indent"
					:icon-class="iconClass"
				></ly-tree-node>
			</view>
		</template>
	</view>
</template>

<script>
import Vue from 'vue';
import TreeStore from './model/tree-store.js';
import { getNodeKey } from './tool/util.js';
import LyTreeNode from './ly-tree-node.vue';

export default {
	name: 'LyTree',

	componentName: 'LyTree',

	components: {
		LyTreeNode
	},

	data() {
		return {
			updateKey: new Date().getTime(), // 数据更新的时候，重新渲染树
			elId: `ly_${Math.ceil(Math.random() * 10e5).toString(36)}`,
			visible: true,
			store: {
				ready: false
			},
			currentNode: null,
			childNodesId: []
		};
	},

	provide() {
		return {
			tree: this
		};
	},

	props: {
		// 展示数据
		treeData: Array,

		// 自主控制loading加载，避免数据还没获取到的空档出现“暂无数据”字样
		ready: {
			type: Boolean,
			default: true
		},

		// 内容为空的时候展示的文本
		emptyText: {
			type: String,
			default: '暂无数据'
		},

		// 是否在第一次展开某个树节点后才渲染其子节点
		renderAfterExpand: {
			type: Boolean,
			default: true
		},

		// 每个树节点用来作为唯一标识的属性，整棵树应该是唯一的
		nodeKey: String,

		// 在显示复选框的情况下，是否严格的遵循父子不互相关联的做法，默认为 false
		checkStrictly: Boolean,

		// 是否默认展开所有节点
		defaultExpandAll: Boolean,

		// 切换全部展开、全部折叠
		toggleExpendAll: Boolean,

		// 是否在点击节点的时候展开或者收缩节点， 默认值为 true，如果为 false，则只有点箭头图标的时候才会展开或者收缩节点
		expandOnClickNode: {
			type: Boolean,
			default: true
		},

		// 选中的时候展开节点
		expandOnCheckNode: {
			type: Boolean,
			default: true
		},

		// 是否在点击节点的时候选中节点，默认值为 false，即只有在点击复选框时才会选中节点
		checkOnClickNode: Boolean,
		checkDescendants: {
			type: Boolean,
			default: false
		},

		// 展开子节点的时候是否自动展开父节点
		autoExpandParent: {
			type: Boolean,
			default: true
		},

		// 默认勾选的节点的 key 的数组
		defaultCheckedKeys: Array,

		// 默认展开的节点的 key 的数组
		defaultExpandedKeys: Array,

		// 是否展开当前节点的父节点
		expandCurrentNodeParent: Boolean,

		// 当前选中的节点
		currentNodeKey: [String, Number],

		// 是否最后一层叶子节点才显示单选/多选框
		checkOnlyLeaf: {
			type: Boolean,
			default: false
		},

		// 节点是否可被选择
		showCheckbox: {
			type: Boolean,
			default: false
		},

		// 节点单选
		showRadio: {
			type: Boolean,
			default: false
		},

		// 配置选项
		props: {
			type: [Object, Function],
			default() {
				return {
					children: 'children', // 指定子树为节点对象的某个属性值
					label: 'label', // 指定节点标签为节点对象的某个属性值
					disabled: 'disabled' //	指定节点选择框是否禁用为节点对象的某个属性值
				};
			}
		},

		// 是否懒加载子节点，需与 load 方法结合使用
		lazy: {
			type: Boolean,
			default: false
		},

		// 是否高亮当前选中节点，默认值是 false
		highlightCurrent: Boolean,

		// 加载子树数据的方法，仅当 lazy 属性为true 时生效
		load: Function,

		// 对树节点进行筛选时执行的方法，返回 true 表示这个节点可以显示，返回 false 则表示这个节点会被隐藏
		filterNodeMethod: Function,

		// 搜索时是否展示匹配项的所有子节点
		childVisibleForFilterNode: {
			type: Boolean,
			default: false
		},

		// 是否每次只打开一个同级树节点展开
		accordion: Boolean,

		// 相邻级节点间的水平缩进，单位为像素
		indent: {
			type: Number,
			default: 12
		},

		// 自定义树节点的展开图标
		iconClass: String,

		// 是否显示节点图标，如果配置为true,需要配置props中对应的图标属性名称
		showNodeIcon: {
			type: Boolean,
			default: false
		},

		// 当节点图标显示出错时，显示的默认图标
		defaultNodeIcon: {
			type: String,
			default: 'https://img-cdn-qiniu.dcloud.net.cn/uniapp/doc/github.svg'
		},

		// 如果数据量较大，建议不要在node节点中添加parent属性，会造成性能损耗
		isInjectParentInNode: {
			type: Boolean,
			default: false
		}
	},

	computed: {
		isEmpty() {
			if (this.store.root) {
				const childNodes = this.store.root.getChildNodes(this.childNodesId);
				return !childNodes || childNodes.length === 0 || childNodes.every(({ visible }) => !visible);
			}
			return true;
		},
		showLoading() {
			return !(this.store.ready && this.ready);
		}
	},

	watch: {
		toggleExpendAll(newVal) {
			this.store.toggleExpendAll(newVal);
		},
		defaultCheckedKeys(newVal) {
			this.store.setDefaultCheckedKey(newVal);
		},
		defaultExpandedKeys(newVal) {
			this.store.defaultExpandedKeys = newVal;
			this.store.setDefaultExpandedKeys(newVal);
		},
		checkStrictly(newVal) {
			this.store.checkStrictly = newVal || this.checkOnlyLeaf;
		},
		'store.root.childNodesId'(newVal) {
			this.childNodesId = newVal;
		},
		'store.root.visible'(newVal) {
			this.visible = newVal;
		},
		childNodesId() {
			this.$nextTick(() => {
				this.$emit('ly-tree-render-completed');
			});
		},
		treeData: {
			handler(newVal) {
				this.updateKey = new Date().getTime();
				this.store.setData(newVal);
				console.log(this.store, '12312312312312312312312');
			},
			deep: true
		}
	},

	methods: {
		/*
		 * @description 对树节点进行筛选操作
		 * @method filter
		 * @param {all} value 在 filter-node-method 中作为第一个参数
		 * @param {Object} data 搜索指定节点的节点数据，不传代表搜索所有节点，假如要搜索A节点下面的数据，那么nodeData代表treeData中A节点的数据
		 */
		filter(value, data) {
			if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter');
			this.store.filter(value, data);
		},

		/*
		 * @description 获取节点的唯一标识符
		 * @method getNodeKey
		 * @param {String, Number} nodeId
		 * @return {String, Number} 匹配到的数据中的某一项数据
		 */
		getNodeKey(nodeId) {
			let node = this.store.root.getChildNodes([nodeId])[0];
			return getNodeKey(this.nodeKey, node.data);
		},

		/*
		 * @description 获取节点路径
		 * @method getNodePath
		 * @param {Object} data 节点数据
		 * @return {Array} 路径数组
		 */
		getNodePath(data) {
			return this.store.getNodePath(data);
		},

		/*
		 * @description 若节点可被选择（即 show-checkbox 为 true），则返回目前被选中的节点所组成的数组
		 * @method getCheckedNodes
		 * @param {Boolean} leafOnly 是否只是叶子节点，默认false
		 * @param {Boolean} includeHalfChecked 是否包含半选节点，默认false
		 * @return {Array} 目前被选中的节点所组成的数组
		 */
		getCheckedNodes(leafOnly, includeHalfChecked) {
			return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
		},

		/*
		 * @description 若节点可被选择（即 show-checkbox 为 true），则返回目前被选中的节点的 key 所组成的数组
		 * @method getCheckedKeys
		 * @param {Boolean} leafOnly 是否只是叶子节点，默认false,若为 true 则仅返回被选中的叶子节点的 keys
		 * @param {Boolean} includeHalfChecked 是否返回indeterminate为true的节点，默认false
		 * @return {Array} 目前被选中的节点所组成的数组
		 */
		getCheckedKeys(leafOnly, includeHalfChecked) {
			return this.store.getCheckedKeys(leafOnly, includeHalfChecked);
		},

		/*
		 * @description 获取当前被选中节点的 data，若没有节点被选中则返回 null
		 * @method getCurrentNode
		 * @return {Object} 当前被选中节点的 data，若没有节点被选中则返回 null
		 */
		getCurrentNode() {
			const currentNode = this.store.getCurrentNode();
			return currentNode ? currentNode.data : null;
		},

		/*
		 * @description 获取当前被选中节点的 key，若没有节点被选中则返回 null
		 * @method getCurrentKey
		 * @return {all} 当前被选中节点的 key， 若没有节点被选中则返回 null
		 */
		getCurrentKey() {
			const currentNode = this.getCurrentNode();
			return currentNode ? currentNode[this.nodeKey] : null;
		},

		/*
		 * @description 设置全选/取消全选
		 * @method setCheckAll
		 * @param {Boolean} isCheckAll 选中状态,默认为true
		 */
		setCheckAll(isCheckAll = true) {
			if (this.showRadio) throw new Error('You set the "show-radio" property, so you cannot select all nodes');

			if (!this.showCheckbox) console.warn('You have not set the property "show-checkbox". Please check your settings');

			this.store.setCheckAll(isCheckAll);
		},

		/*
		 * @description 设置目前勾选的节点
		 * @method setCheckedNodes
		 * @param {Array} nodes 接收勾选节点数据的数组
		 * @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态，默认值为 false
		 */
		setCheckedNodes(nodes, leafOnly) {
			this.store.setCheckedNodes(nodes, leafOnly);
		},

		/*
		 * @description 通过 keys 设置目前勾选的节点
		 * @method setCheckedKeys
		 * @param {Array} keys 勾选节点的 key 的数组
		 * @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态，默认值为 false
		 */
		setCheckedKeys(keys, leafOnly) {
			if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys');
			this.store.setCheckedKeys(keys, leafOnly);
		},

		/*
		 * @description 通过 key / data 设置某个节点的勾选状态
		 * @method setChecked
		 * @param {all} data 勾选节点的 key 或者 data
		 * @param {Boolean} checked 节点是否选中
		 * @param {Boolean} deep 是否设置子节点 ，默认为 false
		 */
		setChecked(data, checked, deep) {
			this.store.setChecked(data, checked, deep);
		},

		/*
		 * @description 若节点可被选择（即 show-checkbox 为 true），则返回目前半选中的节点所组成的数组
		 * @method getHalfCheckedNodes
		 * @return {Array} 目前半选中的节点所组成的数组
		 */
		getHalfCheckedNodes() {
			return this.store.getHalfCheckedNodes();
		},

		/*
		 * @description 若节点可被选择（即 show-checkbox 为 true），则返回目前半选中的节点的 key 所组成的数组
		 * @method getHalfCheckedKeys
		 * @return {Array} 目前半选中的节点的 key 所组成的数组
		 */
		getHalfCheckedKeys() {
			return this.store.getHalfCheckedKeys();
		},

		/*
		 * @description 通过 node 设置某个节点的当前选中状态
		 * @method setCurrentNode
		 * @param {Object} node 待被选节点的 node
		 */
		setCurrentNode(node) {
			if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode');
			this.store.setUserCurrentNode(node);
		},

		/*
		 * @description 通过 key 设置某个节点的当前选中状态
		 * @method setCurrentKey
		 * @param {all} key 待被选节点的 key，若为 null 则取消当前高亮的节点
		 */
		setCurrentKey(key) {
			if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey');
			this.store.setCurrentNodeKey(key);
		},

		/*
		 * @description 根据 data 或者 key 拿到 Tree 组件中的 node
		 * @method getNode
		 * @param {all} data 要获得 node 的 key 或者 data
		 */
		getNode(data) {
			return this.store.getNode(data);
		},

		/*
		 * @description 删除 Tree 中的一个节点
		 * @method remove
		 * @param {all} data 要删除的节点的 data 或者 node
		 */
		remove(data) {
			this.store.remove(data);
		},

		/*
		 * @description 为 Tree 中的一个节点追加一个子节点
		 * @method append
		 * @param {Object} data 要追加的子节点的 data
		 * @param {Object} parentNode 子节点的 parent 的 data、key 或者 node
		 */
		append(data, parentNode) {
			this.store.append(data, parentNode);
		},

		/*
		 * @description 为 Tree 的一个节点的前面增加一个节点
		 * @method insertBefore
		 * @param {Object} data 要增加的节点的 data
		 * @param {all} refNode 要增加的节点的后一个节点的 data、key 或者 node
		 */
		insertBefore(data, refNode) {
			this.store.insertBefore(data, refNode);
		},

		/*
		 * @description 为 Tree 的一个节点的后面增加一个节点
		 * @method insertAfter
		 * @param {Object} data 要增加的节点的 data
		 * @param {all} refNode 要增加的节点的前一个节点的 data、key 或者 node
		 */
		insertAfter(data, refNode) {
			this.store.insertAfter(data, refNode);
		},

		/*
		 * @description 通过 keys 设置节点子元素
		 * @method updateKeyChildren
		 * @param {String, Number} key 节点 key
		 * @param {Object} data 节点数据的数组
		 */
		updateKeyChildren(key, data) {
			if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
			this.store.updateChildren(key, data);
		}
	},

	created() {
		this.isTree = true;

		let props = this.props;
		if (typeof this.props === 'function') props = this.props();
		if (typeof props !== 'object') throw new Error('props must be of object type.');

		this.store = new TreeStore({
			key: this.nodeKey,
			data: this.treeData,
			lazy: this.lazy,
			props: props,
			load: this.load,
			showCheckbox: this.showCheckbox,
			showRadio: this.showRadio,
			currentNodeKey: this.currentNodeKey,
			checkStrictly: this.checkStrictly || this.checkOnlyLeaf,
			checkDescendants: this.checkDescendants,
			expandOnCheckNode: this.expandOnCheckNode,
			defaultCheckedKeys: this.defaultCheckedKeys,
			defaultExpandedKeys: this.defaultExpandedKeys,
			expandCurrentNodeParent: this.expandCurrentNodeParent,
			autoExpandParent: this.autoExpandParent,
			defaultExpandAll: this.defaultExpandAll,
			filterNodeMethod: this.filterNodeMethod,
			childVisibleForFilterNode: this.childVisibleForFilterNode,
			showNodeIcon: this.showNodeIcon,
			isInjectParentInNode: this.isInjectParentInNode
		});

		this.childNodesId = this.store.root.childNodesId;
	},

	beforeDestroy() {
		if (this.accordion) {
			uni.$off(`${this.elId}-tree-node-expand`);
		}
	}
};
</script>

<style>
.ly-tree {
	position: relative;
	cursor: default;
	background: #fff;
	color: #606266;
	padding: 30rpx;
}

.ly-tree.is-empty {
	background: transparent;
}

/* lyEmpty-start */
.ly-empty {
	width: 100%;
	display: flex;
	justify-content: center;
	margin-top: 100rpx;
}
/* lyEmpty-end */

/* lyLoader-start */
.ly-loader {
	margin-top: 100rpx;
	display: flex;
	align-items: center;
	justify-content: center;
}

.ly-loader-inner,
.ly-loader-inner:before,
.ly-loader-inner:after {
	background: #efefef;
	animation: load 1s infinite ease-in-out;
	width: 0.5em;
	height: 1em;
}

.ly-loader-inner:before,
.ly-loader-inner:after {
	position: absolute;
	top: 0;
	content: '';
}

.ly-loader-inner:before {
	left: -1em;
}

.ly-loader-inner {
	text-indent: -9999em;
	position: relative;
	font-size: 22rpx;
	animation-delay: 0.16s;
}

.ly-loader-inner:after {
	left: 1em;
	animation-delay: 0.32s;
}
/* lyLoader-end */

@keyframes load {
	0%,
	80%,
	100% {
		box-shadow: 0 0 #efefef;
		height: 1em;
	}

	40% {
		box-shadow: 0 -1.5em #efefef;
		height: 1.5em;
	}
}
</style>
